﻿/*******************************************************
Copyright © 2008-2009 Active Technologies Group, Inc. (ATGi)

Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
*******************************************************/
using System;
using System.IO;
using System.Linq;
using System.Security.Permissions;
using CommunityServer.Components;

namespace ATGi.CS.Components
{
    /// <summary>
    /// ICSModule that creates preview images of videos.
    /// </summary>
    public class VideoPreviewImageModule : ICSModule
    {
        #region Dependency Helpers

        // The following properties are made available to inject mock objects during testing.
        // (A private accessor must be used to access these properties.)

        static ICentralizedFileStorageProvider _currentCentralizedFileStorageProvider;
        static ICentralizedFileStorageProvider CurrentCentralizedFileStorageProvider
        {
            get
            {
                if (_currentCentralizedFileStorageProvider == null)
                    _currentCentralizedFileStorageProvider = new CentralizedFileStorageProviderWrapper(PostAttachments.CENTRALIZED_FILE_STORAGE_PROVIDER_KEY);

                return _currentCentralizedFileStorageProvider;
            }
            set { _currentCentralizedFileStorageProvider = value; }
        }

        IFileSystemWrapper _currentFileSystemWrapper;
        IFileSystemWrapper CurrentFileSystemWrapper
        {
            get
            {
                if (_currentFileSystemWrapper == null)
                    _currentFileSystemWrapper = new FileSystemWrapper();

                return _currentFileSystemWrapper;
            }
            set { _currentFileSystemWrapper = value; }
        }

        ICSContextWrapper _currentCSContextWrapper;
        ICSContextWrapper CurrentCSContextWrapper
        {
            get
            {
                if (_currentCSContextWrapper == null)
                    _currentCSContextWrapper = new CSContextWrapper();

                return _currentCSContextWrapper;
            }
            set { _currentCSContextWrapper = value; }
        }

        #endregion

        const int DEFAULT_START_TIME_OFFSET = 10;
        const int DEFAULT_PROCESS_TIMEOUT = 10;

        string _ffmpegPath;
        string _tempFilePath;
        int _startTimeOffset = DEFAULT_START_TIME_OFFSET;
        int _processTimeout = DEFAULT_PROCESS_TIMEOUT;

        public static ICentralizedFile GetPreviewFile(PostAttachment attachment)
        {
            // Verify the attachment argument.
            if (attachment == null)
                throw new ArgumentNullException("attachment");

            // Don't return preview files for temporary post attachments.
            if (attachment.PostID == 0)
                return null;

            // Retrieve the centralized file for the preview image.
            ICentralizedFileStorageProvider cfsProvider = CurrentCentralizedFileStorageProvider;

            return cfsProvider.GetFile(
                cfsProvider.MakePath(attachment.PostID),
                GetPreviewFileName(attachment.FileName));
        }
        public static ICentralizedFile GetPreviewFile(ICentralizedFile file)
        {
            if (file == null)
                return null;

            ICentralizedFileStorageProvider cfsProvider = CurrentCentralizedFileStorageProvider;

            return cfsProvider.GetFile(file.Path, GetPreviewFileName(file.FileName));
        }

        public void Init(CSApplication csa, System.Xml.XmlNode node)
        {
            // Parse file path attributes.
            ParseAndVerifyFFMpegPathAttribute(node);
            ParseAndVerifyTempFilePathAttribute(node);

            // Parse the start time offset, if specified.
            if (node.Attributes["startTimeOffset"] != null)
                int.TryParse(node.Attributes["startTimeOffset"].Value, out _startTimeOffset);

            // Parse the process timeout, if specified.
            if (node.Attributes["processTimeout"] != null)
                int.TryParse(node.Attributes["processTimeout"].Value, out _processTimeout);

            // Add handlers for the module.
            csa.PostPostUpdate += new CSPostEventHandler(csa_PostPostUpdate);
        }

        protected internal virtual void csa_PostPostUpdate(IContent content, CSPostEventArgs e)
        {
            // Only process the post if it is being created or updated.
            if (e.State == ObjectState.Create ||
                e.State == ObjectState.Update)
            {
                Post post = content as Post;

                // Only process the post if it has an attachment,
                if (post != null && post.Attachment != null && post.PostMedia == PostMediaType.Video)
                {
                    // Create a preview file for the post.
                    System.Threading.ThreadPool.QueueUserWorkItem(new System.Threading.WaitCallback(CreatePreviewFile), post);
                }
            }
        }
        protected internal virtual void CreatePreviewFile(object postObject)
        {
            Post post = postObject as Post;

            if (post != null)
            {
                IFileSystemWrapper fs = CurrentFileSystemWrapper;

                // Create a temporary folder.
                string temporaryFolderPath = Path.Combine(_tempFilePath, Guid.NewGuid().ToString());

                fs.CreateDirectory(temporaryFolderPath);

                try
                {
                    // Determine paths to temporary files.
                    PostAttachment attachment = post.Attachment;

                    string temporaryVideoFilePath = Path.Combine(temporaryFolderPath, attachment.PostID.ToString());
                    string temporaryPreviewFilePath = Path.Combine(temporaryFolderPath, GetPreviewFileName(attachment.PostID.ToString()));
                    string attachmentPath = CurrentCentralizedFileStorageProvider.MakePath(attachment.PostID);

                    string previewFileName = GetPreviewFileName(attachment.FileName);

                    // Write the attachment to a local file.
                    using (Stream attachmentStream = OpenAttachmentStream(attachment))
                        fs.WriteStreamToFile(attachmentStream, temporaryVideoFilePath);

                    CreateTemporaryPreviewFile(post.GetVideoPreviewImageTimeOffset(), temporaryVideoFilePath, temporaryPreviewFilePath);

                    DeleteExistingPreviewFilesFromCentralizedFileStorage(attachmentPath, previewFileName);

                    SavePreviewFileToCentralizedFileStorage(temporaryPreviewFilePath, attachmentPath, previewFileName);
                }
                catch (Exception e)
                {
                    HandleException(post, e);
                }
                finally
                {
                    // Delete temporary folder.
                    fs.DeleteDirectoryIfExists(temporaryFolderPath);
                }
            }
        }

        protected internal virtual Stream OpenAttachmentStream(PostAttachment attachment)
        {
            if (attachment.IsRemote)
            {
                using (System.Net.WebClient wc = new System.Net.WebClient())
                    return wc.OpenRead(attachment.FileName);
            }
            else
                return attachment.OpenReadStream();
        }
        protected internal virtual IFFMpegProcessWrapper CreateFFMpegProcessWrapper()
        {
            return new FFMpegProcessWrapper();
        }
        protected internal virtual void HandleException(Post post, Exception e)
        {
            CSException cse = new CSException(CSExceptionType.UnknownError, string.Format("Error creating video preview image for post {0}: {1}", post.PostID, e.Message), e);
            cse.Log();
        }
        
        protected internal virtual void ParseAndVerifyFFMpegPathAttribute(System.Xml.XmlNode node)
        {
            const string ATTR_NAME = "ffmpegPath";

            IFileSystemWrapper fs = CurrentFileSystemWrapper;

            // Verify that FFMPEG is found at the configured location.
            if (node.Attributes[ATTR_NAME] != null)
                _ffmpegPath = node.Attributes[ATTR_NAME].Value;
            else
                _ffmpegPath = CurrentCSContextWrapper.MapPath("~/bin/");

            // Verify that ffmpeg.exe exists at the  path.
            if (!fs.FileExists(_ffmpegPath))
            {
                if (!fs.DirectoryExists(_ffmpegPath))
                    throw new InvalidOperationException(String.Format("The specified {0} '{1}' does not exist or is not accessible.", ATTR_NAME, _ffmpegPath));
                else
                {
                    _ffmpegPath = Path.Combine(_ffmpegPath, "ffmpeg.exe");

                    if (!fs.FileExists(_ffmpegPath))
                        throw new InvalidOperationException(String.Format("The specified {0} '{1}' does not contain ffmpeg.exe.", ATTR_NAME, _ffmpegPath));
                }                    
            }

            // Verify that the current user has the right to read the path.
            fs.DemandFileIOPermission(FileIOPermissionAccess.Read, _ffmpegPath);
        }
        protected internal virtual void ParseAndVerifyTempFilePathAttribute(System.Xml.XmlNode node)
        {
            const string ATTR_NAME = "tempFilePath";

            IFileSystemWrapper fs = CurrentFileSystemWrapper;

            // Verify that the temp file path exists.
            // (Use the default file system file storage path if none is specified.)
            if (node.Attributes[ATTR_NAME] != null)
                _tempFilePath = node.Attributes[ATTR_NAME].Value;
            else
                _tempFilePath = CurrentCSContextWrapper.MapPath("~/filestorage/");


            if (!fs.DirectoryExists(_tempFilePath))
                throw new InvalidOperationException(String.Format("The {0} '{1}' does not exist or is not accessible.", ATTR_NAME, _tempFilePath));
            else
            {
                // Verify that the path is writeable.
                fs.DemandFileIOPermission(FileIOPermissionAccess.AllAccess, _tempFilePath);
            }
        }

        protected internal virtual void DeleteExistingPreviewFilesFromCentralizedFileStorage(string attachmentPath, string previewFileName)
        {
            // Delete files that are derived from the preview file, (e.g. resized images).
            foreach (ICentralizedFile file in CurrentCentralizedFileStorageProvider.GetFiles(attachmentPath, PathSearchOption.TopLevelPathOnly))
            {
                if (file.FileName.StartsWith(previewFileName))
                    CurrentCentralizedFileStorageProvider.Delete(file.Path, file.FileName);
            }
        }
        protected internal virtual void SavePreviewFileToCentralizedFileStorage(string previewPath, string attachmentPath, string previewFileName)
        {
            IFileSystemWrapper fs = CurrentFileSystemWrapper;

            // Upload the preview file alongside the centralized file.
            // (Even if the attachment is remote, upload the preview where a local attachment would be.)
            using (Stream previewStream = fs.FileOpenRead(previewPath))
            {
                CurrentCentralizedFileStorageProvider.AddUpdateFile(
                    attachmentPath,
                    previewFileName,
                    previewStream);

                previewStream.Close();
            }
        }

        protected internal virtual void CreateTemporaryPreviewFile(int startTimeOffset, string videoPath, string previewPath)
        {
            // Determine the start time offset.
            // (If not specified in the post attributes, use the default.)
            if (startTimeOffset < 0)
                startTimeOffset = _startTimeOffset;

            // Execute the FFMpeg process.
            IFFMpegProcessWrapper ffmpegProcess = CreateFFMpegProcessWrapper();

            ffmpegProcess.FFMpegPath = _ffmpegPath;
            ffmpegProcess.ProcessTimeout = _processTimeout;

            ffmpegProcess.InputVideoPath = videoPath;
            ffmpegProcess.OutputImagePath = previewPath;
            ffmpegProcess.StartTimeOffset = startTimeOffset;

            ffmpegProcess.Execute();
        }

        private static string GetPreviewFileName(string videoFileName)
        {
            // If the attachment is a URI, then get just the file name.
            if (Uri.IsWellFormedUriString(videoFileName, UriKind.Absolute))
            {
                Uri attachmentUri = new Uri(videoFileName);
                videoFileName = attachmentUri.Segments.Last();
            }

            // Replace the attachment extension with ".jpg".
            FileInfo attachmentFileInfo = new FileInfo(videoFileName);
            return attachmentFileInfo.Name.Substring(0, attachmentFileInfo.Name.Length - attachmentFileInfo.Extension.Length) + ".jpg";
        }
    }
}
